 <template>
   <div class="deploymentDetail_container">
    <div class="tech-back-btn" @click="handleBack">
       <div class="btn-content">
         <el-icon :size="18"><ArrowLeftBold /></el-icon>
         <span>返回</span>
       </div>
       <div class="border-beam"></div>
     </div>
     <MCard class="m-t-20px">
      <template #header>
        <div class="deployPlatformDetail_header">
          <span>部署详情</span>
        </div>
      </template>
      <div>
        <div class="tech-info-display">
          <!-- 数据展示区域 -->
          <div class="tech-info-section">
            <div class="tech-info-item" v-for="(_,key) in deployInfoItems" :key="key" v-show="key!=='commitMessage'">
              <div class="info-label">
                <div class="label-icon"></div>
                <span>{{ deployInfoItems[key] }}:</span>
              </div>
              <div class="info-value">{{ deployInfo[key]}}</div>
            </div>
          </div>
          <!-- 提交信息区域 -->
          <div class="tech-info-message">
            <div class="message-label">
              <span>提交信息:</span>
              <div class="scanning-line"></div>
            </div>
            <div class="message-content">
              <p>{{ deployInfo.commitMessage || '该模块被编译被编译被编译被编译被编译被编译被编译被编译被编译被编译' }}</p>
            </div>
          </div>
          <!-- 装饰性元素 -->
          <div class="tech-decoration top-left"></div>
          <div class="tech-decoration top-right"></div>
          <div class="tech-decoration bottom-left"></div>
          <div class="tech-decoration bottom-right"></div>
        </div>
      </div>
    </MCard>
    <MCard>
      <template #header>
        <div class="flex gap-20px items-center">
          <span>节点信息</span>
        </div>
      </template>
      <div class="flex gap-20px items-center">
        <!-- 传参区分主动停止还是切换停止 -->
        <!-- <div class="global_btn"  v-globalBtnLoading="{handler:handleLookNodeLog,text:'获取日志中...'}">查看节点日志</div> -->
        <!-- <div class="global_btn" v-if="logList.length" v-globalBtnLoading="{handler:stopLastNodeLog.bind(null,true),text:'停止获取日志中...'}">停止获取日志</div> -->
      </div>
      <div class="grid grid-cols-4 gap-10px m-t-20px">
        <div class="deployedNodeItem" v-for="(item,index) in deployedNodeList" :key="index">
            <div class="node-header">
             <!--  <div class="tech-checkbox" @click="toggleItemSelect(item)">
                <div class="checkbox-inner" :class="{'is-checked': item.isSelect}">
                  <div class="check-mark" v-if="item.isSelect"></div>
                </div>
              </div> -->
              <span class="node-id">节点ID: {{ item.nodeId }}</span>
            </div>
            <div class="node-divider"></div>
            <div class="node-content">
              <div class="node-info">
                <div class="info-grid">
                  <div class="info-item">
                    <span class="info-label">节点类型:</span>
                    <span class="info-value" :class="item.type===1 ? 'real-node' : 'virtual-node'">{{ item.type===1 ? '真实节点' : '虚拟节点' }}</span>
                  </div>
                  <div class="info-item" v-for="(_,key) in deployedNodeListItems" :key="key">
                    <span class="info-label">{{ deployedNodeListItems[key] }}:</span>
                    <span class="info-value">{{ item[key] }}</span>
                  </div>
                </div>
                <div class="status-indicator" :class="item.connectionStatus === 1 ? 'connected' : 'disconnected'">
                  <img :src="item.imgUrl" alt="" class="status-icon">
                  <div class="status-pulse"></div>
                  <div class="status-text">{{ item.connectionStatus === 1 ? '已连接' : '未连接' }}</div>
                </div>
              </div>
              <div class="node-actions">
                <div class="global_btn"  v-globalBtnLoading="{handler:handleLookNodeLog.bind(null,item),text:'获取日志中...'}">查看节点日志</div>
                <div class="global_btn" v-show="item.isShowStopLog" v-globalBtnLoading="{handler:stopLastNodeLog.bind(null,item),text:'停止获取日志中...'}">停止节点日志</div>
              </div>
            </div>
        </div>
      </div>
       <div class="flex justify-center m-t-20px">
        <el-pagination
            v-model:currentPage="deployedNodePageConfig.currentPage"
            v-model:page-size="deployedNodePageConfig.pageSize"
            :page-sizes="[3,5,7,10]"
            layout="total, prev, pager, next, jumper"
            :total="deployedNodePageConfig.total"
            @size-change="getDeployedNodeList"
            @current-change="getDeployedNodeList"
            size="small"
           />
       </div>
    </MCard>
    <MCard>
      <template #header>
        <div class="flex gap-20px items-center">
          <span>节点{{lastAndCurrentNodeIds.currentNodeIds}}日志信息</span>
        </div>
      </template>
      <div >
      <div class="log_container" ref="containerRef" @scroll="onScroll"
      style="padding-bottom: 50px;">
      <div class="virtual-list" :style="{ height: totalHeight + 'px' }">
        <div class="virtual-list-phantom" :style="{ transform: `translateY(${offsetY}px)` }">
          <div v-for="item in visibleData" :key="item.index" class="log-item">
            <span class="log-index">[{{ item.index + 1 }}]</span>
            <span class="log-content">{{ item.content }}</span>
          </div>
        </div>
      </div>
      </div>
      <div class="flex justify-between items-center m-t-20px">
         <div class="flex gap-20px items-center">
           <div class="global_btn" @click="handleExportLog">导出日志</div>
           <div class="global_btn" @click="handleClearLog">清除日志</div>
         </div>
         <transition name="fade">
          <div class="global_btn filtered-scroll-btn"
            v-if="!isAtBottom && logList.length > 0" @click="scrollToBottom">
            <span>滚到到底部</span>
            <el-icon><ArrowDown /></el-icon>
          </div>
        </transition>
      </div>
      </div>
    </MCard>
   </div>
 </template>
 
 <script lang="ts">
 import { defineComponent } from 'vue'
 export default defineComponent({
   name: 'DeploymentDetail',
 })
 </script>
 <script setup  lang="ts">
 import { useRoute,useRouter } from "vue-router"
 import { deployPlatformApi } from "@/api/deployPlatform"
 import { nodeManagementApi } from "@/api/nodeManagement"
 import { LogApi } from "@/api/log.ts"
import connectedImg from "@/assets/imgs/connected.png"
import disconnectedImg from "@/assets/imgs/disconnected.png"
import { getWebSocketInstance } from "@/utils/websocketExample"
import { throttle } from "lodash"
import { getUserInfo } from "@/utils/token-utils"
import { ElNotificationFn } from '@/utils/computedUtils'
import { nodeDeploymentApi } from "@/api/nodeDeployment"
const userInfo = getUserInfo()
const route = useRoute()
const router = useRouter()
const deploymentId = route.query.deploymentId
const itemHeight = 36 // 增加每项高度，留出间距
const bufferSize = 5 // 上下缓冲区大小
const visibleCount = 30 // 固定显示30条
const containerRef = ref<HTMLElement | null>(null)
const scrollTop = ref(0) // 滚动条位置
const logList = ref<string[]>([]) // 日志列表
const contentLength = ref(0) //数据总条数
const totalHeight = computed(() => logList.value.length * itemHeight) // 计算总高度
const isUserScrolling = ref(false) // 判断用户是否正在主动滚动
const isAtBottom = ref(true) // 判断是否在底部
const isInitialLoad = ref(true) // 是否是初始加载
const getVisibleRange = () => {  // 计算可见范围
  const start = Math.floor(scrollTop.value / itemHeight)
  return {
    start: Math.max(0, start - bufferSize),
    end: Math.min(logList.value.length, start + visibleCount + bufferSize)
  }
}
const offsetY = computed(() => { // 计算偏移量
  const { start } = getVisibleRange()
  return start * itemHeight
})
const visibleData = computed(() => {
  const { start, end } = getVisibleRange()
  return logList.value.slice(start, end).map((content, index) => {
    // 计算实际索引：当前切片的起始位置 + 在切片中的相对位置
    const actualIndex = start + index; 
    return {
      content,
      index: actualIndex // 或者用Math.max(0, contentLength.value - (logList.value.length - actualIndex))
    };
  })
})
const throttledScrollHandler = throttle((e: Event) => {
  if (!containerRef.value) return
  const target = e.target as HTMLElement
  scrollTop.value = target.scrollTop
  const isNearBottom = target.scrollHeight - target.scrollTop - target.clientHeight < 20
  isAtBottom.value = isNearBottom
  if (!isNearBottom) {
    isUserScrolling.value = true
  } else {
    isUserScrolling.value = false
  }
}, 200)
const onScroll = (e: Event) => {
  throttledScrollHandler(e)
}
const scrollToBottom = async () => {
  if (containerRef.value) {
    await nextTick()
    const container = containerRef.value
    const scrollHeight = container.scrollHeight
    const clientHeight = container.clientHeight  
    if(isInitialLoad.value){
      container.scrollTop = scrollHeight - clientHeight
    }else{
      container.scrollTo({
        top: scrollHeight - clientHeight,
        behavior: 'smooth'
      })
    }
  }
}
 const deployInfo = ref<any>({
  deploymentName:"",
  deploymentId:"",
  versionNumber:"",
  commitId:"",
  createdTime:"",
  commitMessage:""
})
const deployInfoItems  = ref<any>({
  deploymentName:"文件名称",
  deploymentId:"文件ID",
  versionNumber:"版本号",
  commitId:"提交ID",
  createdTime:"创建时间",
  commitMessage:"提交信息"
})
 async function getDeployInfo(){
  try {
    const {data} = await deployPlatformApi.getDeployPlatformListByCondition({
      deploymentId
    })
    deployInfo.value = data[0]
  } catch (error) {
    
  }
}
function handleBack(){
  router.back()
}
const deployedNodePageConfig = ref<any>({
  pageSize:4,
  currentPage:1,
  total:0
})
const deployedNodeList = ref<any>([])
const deployedNodeListItems = ref<any>({
  ipAddress:"IP地址",
  deploymentFileNumber:"部署文件",
  updateTime:"更新时间",
})
// 获取节点全量日志
async function getDeployedNodeLog(){
  try {
    logList.value = []
    const {data} = await LogApi.getNodeLog({
      nodeId:lastAndCurrentNodeIds.value.currentNodeIds,
      deploymentId,
      userId:userInfo.userid
  })
  if(!data.logs) return
  logList.value.push(...data.logs.split("\n"))
  pid.value = data.pid
  } catch (error) { }
}
// 获取已部署节点列表
async function getDeployedNodeList(){
  try {
    const {data} = await nodeManagementApi.postDeployedNodeList({
    pageSize:deployedNodePageConfig.value.pageSize,
    currentPage:deployedNodePageConfig.value.currentPage,
    deploymentId,
  })
  deployedNodeList.value =  data.records.filter((item:any)=>item.connectionStatus=== 1).map((item:any,index:number)=>{
    return {
      ...item,
      isShowStopLog:index===0?true:false,
      imgUrl:item.connectionStatus=== 1 ? connectedImg : disconnectedImg
    }
  })
  lastAndCurrentNodeIds.value.currentNodeIds = deployedNodeList.value[0].nodeId
  deployedNodePageConfig.value.total = data.total
  getDeployedNodeLog()
  } catch (error) {}
}
async function stopLastNodeLog(item?:any){
  try {
    await deployPlatformApi.issueStopDeployedNodeLog({
      deploymentId,
      nodeId:item?.nodeId?item.nodeId:lastAndCurrentNodeIds.value.lastNodeIds,
      userId:userInfo.userid
    })
    item.isShowStopLog = false
    if(item){
      lastAndCurrentNodeIds.value.isStop = true
    }
    ElNotificationFn('success','日志停止成功')
  } catch (error){ }
}
const pid:any = ref('')
/* async function getNodeDeploymentResultByCondition(){
  try {
    const {data} = await nodeDeploymentApi.getNodeDeploymentResultByCondition({
      deploymentId,
      nodeId:lastAndCurrentNodeIds.value.currentNodeIds
    })
    PID.value = data.PID
  } catch (error) {  }
} */
const wsClient = getWebSocketInstance()
const lastAndCurrentNodeIds = ref<any>({
  lastNodeIds:"",
  currentNodeIds:"",
  isStop:false
})
async function handleLookNodeLog(row:any){
  try {
   lastAndCurrentNodeIds.value.lastNodeIds = lastAndCurrentNodeIds.value.currentNodeIds
   lastAndCurrentNodeIds.value.currentNodeIds = row.nodeId
   if(!lastAndCurrentNodeIds.value.isStop){
    await stopLastNodeLog()
   }
   await getDeployedNodeLog()
  //  await getNodeDeploymentResultByCondition()
   deployedNodeList.value.forEach((item:any)=>{
    item.isShowStopLog = false
   })
   row.isShowStopLog = true
   lastAndCurrentNodeIds.value.isStop = false
   ElNotificationFn('success','日志获取成功')
  } catch (error) {}
}
function handleExportLog(){
  if(!logList.value.length) return ElNotificationFn('warning','日志为空')
  const logContent = logList.value.join("\n")
  const blob = new Blob([logContent],{type:"text/plain"})
  const a = document.createElement('a');
  a.href = window.URL.createObjectURL(blob);
  a.download = `部署物${deployInfo.value.deploymentName}-${deployInfo.value.versionNumber}.log`
  document.body.appendChild(a);
  a.click()
  document.body.removeChild(a);
  window.URL.revokeObjectURL(a.href);
}
function handleClearLog(){
  logList.value = []
}
const  wsResponseHandlers:any = {
  "JRLC.logs.app":handleNodeLog
}
function handleNodeLog(data:any){
   if(!data.logs) return
   if(data.PID!=pid.value) return
   logList.value.push(data.timestamp+":"+data.logs)
   if (!isUserScrolling.value || isAtBottom.value) {
    nextTick(() => {
      scrollToBottom()
    })
  }
}
wsClient.on('message',(resPonse:any)=>{
  const {cmd,data} = resPonse
  if(!Object.keys(wsResponseHandlers).includes(cmd)) return
  wsResponseHandlers[cmd](data)
})
onMounted(async ()=>{
  await getDeployInfo()
  await getDeployedNodeList()
})
onUnmounted(()=>{
  stopLastNodeLog()
})
 </script>
 
 <style  lang="scss" scoped>
 @import "@/styles/card.scss";
 @import "@/styles/pagination.scss";
 @import "@/styles/input.scss";

 // 科技感数据展示样式
.tech-info-display {
  position: relative;
  padding: 20px;
  background: rgba(16, 25, 38, 0.4);
  border-radius: 4px;
  border: 1px solid rgba(35, 85, 140, 0.3);
  box-shadow: inset 0 0 20px rgba(0, 140, 255, 0.05);
  overflow: hidden;
  &::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-image: 
      radial-gradient(rgba(0, 180, 255, 0.03) 1px, transparent 1px),
      radial-gradient(rgba(0, 180, 255, 0.03) 1px, transparent 1px);
    background-size: 20px 20px;
    background-position: 0 0, 10px 10px;
    pointer-events: none;
    z-index: 1;
  }
}

.tech-info-section {
  position: relative;
  z-index: 2;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 20px;
  margin-bottom: 25px;
}

.tech-info-item {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px 15px;
  background: linear-gradient(
    90deg,
    rgba(20, 32, 48, 0.5) 0%,
    rgba(25, 40, 60, 0.5) 100%
  );
  border-radius: 4px;
  border: 1px solid rgba(35, 85, 140, 0.3);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
  transition: all 0.3s;
  
  &::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: 3px;
    height: 100%;
    background: linear-gradient(
      to bottom,
      rgba(0, 150, 255, 0.2),
      rgba(0, 180, 255, 0.5),
      rgba(0, 150, 255, 0.2)
    );
    border-radius: 4px 0 0 4px;
  }
  
  &:hover {
    transform: translateY(-2px);
    box-shadow: 
      0 4px 15px rgba(0, 0, 0, 0.3),
      0 0 10px rgba(0, 150, 255, 0.1);
    
    &::before {
      background: linear-gradient(
        to bottom,
        rgba(0, 150, 255, 0.3),
        rgba(0, 210, 255, 0.7),
        rgba(0, 150, 255, 0.3)
      );
    }
    
    .info-value {
      text-shadow: 0 0 8px rgba(0, 180, 255, 0.5);
    }
  }
}

.info-label {
  display: flex;
  align-items: center;
  gap: 8px;
  color: rgba(0, 180, 255, 0.8);
  font-size: 14px;
  font-weight: 500;
  
  .label-icon {
    width: 6px;
    height: 6px;
    background: rgba(0, 180, 255, 0.8);
    border-radius: 1px;
    position: relative;
    transform: rotate(45deg);
    
    &::after {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 3px;
      height: 3px;
      background: rgba(0, 210, 255, 0.9);
      transform: translate(-50%, -50%);
      border-radius: 1px;
      box-shadow: 0 0 3px rgba(0, 210, 255, 0.8);
    }
  }
}

.info-value {
  color: #ffffff;
  font-size: 15px;
  font-family: 'Consolas', monospace;
  letter-spacing: 0.5px;
  padding-left: 14px;
  position: relative;
  text-shadow: 0 0 5px rgba(0, 180, 255, 0.3);
  transition: all 0.3s;
  
  &::before {
    content: '';
    position: absolute;
    left: 0;
    top: 50%;
    transform: translateY(-50%);
    width: 8px;
    height: 1px;
    background: rgba(0, 180, 255, 0.5);
  }
}

.tech-info-message {
  position: relative;
  z-index: 2;
  padding: 15px;
  background: rgba(18, 30, 45, 0.4);
  border: 1px solid rgba(35, 85, 140, 0.4);
  border-radius: 4px;
  margin-top: 15px;
  box-shadow: 
    inset 0 0 15px rgba(0, 0, 0, 0.2),
    0 2px 10px rgba(0, 0, 0, 0.2);
}

.message-label {
  position: relative;
  color: rgba(0, 180, 255, 0.8);
  font-size: 14px;
  font-weight: 500;
  margin-bottom: 12px;
  padding-bottom: 8px;
  border-bottom: 1px solid rgba(35, 85, 140, 0.3);
  
  .scanning-line {
    position: absolute;
    bottom: -1px;
    left: 0;
    width: 30%;
    height: 1px;
    background: rgba(0, 210, 255, 0.8);
    box-shadow: 0 0 5px rgba(0, 210, 255, 0.5);
    animation: scan-line 3s infinite alternate ease-in-out;
  }
}

.message-content {
  color: rgba(255, 255, 255, 0.85);
  font-size: 14px;
  line-height: 1.5;
  padding: 5px 0;
  font-family: 'PingFang SC', Arial, sans-serif;
  position: relative;
  
  p {
    margin: 0;
    position: relative;
    z-index: 2;
  }
  
  &::before {
    content: '';
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    background: linear-gradient(
      to left,
      rgba(18, 30, 45, 0) 0%,
      rgba(0, 180, 255, 0.02) 100%
    );
  }
}

// 装饰性元素
.tech-decoration {
  position: absolute;
  width: 15px;
  height: 15px;
  
  &.top-left {
    top: -1px;
    left: -1px;
    border-top: 2px solid rgba(0, 170, 255, 0.7);
    border-left: 2px solid rgba(0, 170, 255, 0.7);
    border-top-left-radius: 3px;
  }
  
  &.top-right {
    top: -1px;
    right: -1px;
    border-top: 2px solid rgba(0, 170, 255, 0.7);
    border-right: 2px solid rgba(0, 170, 255, 0.7);
    border-top-right-radius: 3px;
  }
  
  &.bottom-left {
    bottom: -1px;
    left: -1px;
    border-bottom: 2px solid rgba(0, 170, 255, 0.7);
    border-left: 2px solid rgba(0, 170, 255, 0.7);
    border-bottom-left-radius: 3px;
  }
  
  &.bottom-right {
    bottom: -1px;
    right: -1px;
    border-bottom: 2px solid rgba(0, 170, 255, 0.7);
    border-right: 2px solid rgba(0, 170, 255, 0.7);
    border-bottom-right-radius: 3px;
  }
}

.tech-scanner {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  background: linear-gradient(
    90deg,
    transparent 0%,
    rgba(0, 170, 255, 0.5) 50%,
    transparent 100%
  );
  opacity: 0.6;
  box-shadow: 0 0 8px rgba(0, 170, 255, 0.3);
  animation: vertical-scan 6s linear infinite;
  z-index: 10;
}

.deployedNodeItem {
  background: linear-gradient(
    180deg, 
    rgba(22, 30, 45, 0.8) 0%, 
    rgba(18, 28, 40, 0.85) 50%, 
    rgba(15, 25, 38, 0.8) 100%
  );
  border: 1px solid rgba(35, 85, 140, 0.5);
  border-radius: 4px;
  box-shadow: 
    inset 0 0 15px rgba(0, 140, 255, 0.08),
    0 0 10px rgba(0, 0, 0, 0.4),
    0 0 8px rgba(0, 150, 255, 0.1);
  overflow: hidden;
  position: relative;
  transition: all 0.3s ease;
  
  &::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 1px;
    background: linear-gradient(90deg, 
      transparent, 
      rgba(0, 170, 255, 0.5), 
      transparent
    );
    opacity: 0.7;
    z-index: 2;
  }
  
  &::after {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-image: 
      linear-gradient(rgba(40, 100, 180, 0.02) 1px, transparent 1px),
      linear-gradient(90deg, rgba(40, 100, 180, 0.02) 1px, transparent 1px);
    background-size: 20px 20px;
    pointer-events: none;
    opacity: 0.5;
    z-index: 1;
  }
  
  &:hover {
    transform: translateY(-2px);
    box-shadow: 
      inset 0 0 20px rgba(0, 140, 255, 0.12),
      0 5px 15px rgba(0, 0, 0, 0.4),
      0 0 15px rgba(0, 150, 255, 0.15);
    
    .node-id {
      color: #fff;
      text-shadow: 0 0 8px rgba(0, 180, 255, 0.6);
    }
    
    .node-divider {
      background: linear-gradient(90deg, 
        transparent, 
        rgba(0, 180, 255, 0.6), 
        transparent
      );
      box-shadow: 0 0 8px rgba(0, 180, 255, 0.4);
    }
  }
  
  .node-header {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px;
    position: relative;
    z-index: 2;
  }
  
  .node-id {
    color: rgba(255, 255, 255, 0.9);
    font-family: 'Consolas', monospace;
    font-size: 14px;
    font-weight: 500;
    letter-spacing: 0.5px;
    text-shadow: 0 0 5px rgba(0, 140, 255, 0.3);
    transition: all 0.3s;
  }
  
  .node-divider {
    height: 1px;
    width: 90%;
    margin: 0 auto;
    background: linear-gradient(90deg, 
      transparent, 
      rgba(0, 150, 255, 0.4), 
      transparent
    );
    transition: all 0.3s;
  }
  
  .node-content {
    padding: 12px;
    position: relative;
    z-index: 2;
    display: flex;
    flex-direction: column;
    gap: 15px;
  }
  
  .node-info {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  
  .info-grid {
    display: grid;
    gap: 8px;
  }
  
  .info-item {
    display: flex;
    align-items: center;
    gap: 8px; 
    .real-node {
      color: rgba(0, 255, 80, 0.9);
      text-shadow: 
        0 0 5px rgba(0, 255, 80, 0.5),
        0 0 10px rgba(0, 255, 80, 0.3);
      font-weight: 500;
      letter-spacing: 0.5px;
      position: relative;
      padding-left: 15px;
    } 
    .virtual-node {
      color: rgba(255, 80, 80, 0.9);
      text-shadow: 
        0 0 5px rgba(255, 80, 80, 0.5),
        0 0 10px rgba(255, 80, 80, 0.3);
      font-weight: 500;
      letter-spacing: 0.5px;
      position: relative;
      padding-left: 15px;
    }
  }
  
  .info-label {
    color: rgba(100, 180, 255, 0.9);
    font-size: 13px;
    font-weight: 500;
  }
  
  .info-value {
    color: rgba(255, 255, 255, 0.85);
    font-size: 13px;
    font-family: 'Consolas', monospace;
  }
  
  .status-indicator {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    position: relative;
    width: 50px;
    height: 50px;
    
    &.connected {
      .status-icon {
        filter: drop-shadow(0 0 3px #00ff4c);
      }    
      /* .status-pulse {
        border: 2px solid rgba(0, 255, 76, 0.7);
        animation: pulse-green 2s infinite;
      }  */
      .status-text {
        color: #01AA78; 
      }
    }   
    &.disconnected {
      .status-icon {
        filter: drop-shadow(0 0 3px #ff4545);
      }      
    /*   .status-pulse {
        border: 2px solid rgba(255, 69, 69, 0.7);
        animation: pulse-red 2s infinite;
      }      */
      .status-text {
        color: #DD675E;
      }
    }
  }
  
  .status-icon {
    width: 24px;
    height: 24px;
    position: relative;
    z-index: 2;
    margin-bottom: 5px;
  }
  
  .status-text {
    font-size: 12px;
    font-weight: 500;
    letter-spacing: 0.5px;
    font-family: 'Consolas', monospace;
    position: relative;
    z-index: 2;
    white-space: nowrap;
  }
  
  .status-pulse {
    position: absolute;
    top: 12px; /* 调整位置以匹配图标 */
    left: 50%;
    transform: translate(-50%, -50%);
    width: 30px;
    height: 30px;
    border-radius: 50%;
    opacity: 0;
  }
  
  .node-actions {
    display: flex;
    gap: 10px;
    margin-top: 5px;
  }
}


.log_container {
    flex: 1;
    position: relative;
    background: rgba(0, 17, 45, 0.5);
    overflow: auto;
    padding: 10px;
    border: 1px solid rgba(31, 106, 178, 0.3);
    border-radius: 4px;
    max-height: calc(100% - 40px); // 减去标题高度
    min-height: 0; // 允许容器收缩
    height: 350px;
    &::-webkit-scrollbar {
      width: 8px;
      height: 8px;
    }

    &::-webkit-scrollbar-thumb {
      background: rgba(255, 255, 255, 0.2);
      border-radius: 4px;
      
      &:hover {
        background: rgba(255, 255, 255, 0.3);
      }
    }

    &::-webkit-scrollbar-track {
      background: transparent;
    }
    .virtual-list {
    position: relative;
    width: 100%;
  }

  .virtual-list-phantom {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    z-index: 0;
    min-width: 100%;
  }

  .log-item {
    padding: 6px 20px;
    margin-top: 4px;
    line-height: 20px;
    margin-bottom: 0;
    height: 32px;
    box-sizing: border-box;
    white-space: nowrap;
    min-width: 100%;
    color: #fff;
    border-radius: 4px;
    transition: background-color 0.2s; // 修改这行
    &:hover {
      background-color: rgba(255, 255, 255, 0.1);
    }

    .log-index {
      display: inline-block;
      min-width: 50px;
      margin-right: 8px;
      color: #384b5e;
      font-family: monospace;
      font-size: 12px;
    }

    .log-content {
      font-size: 16px;
      color: rgba(255, 255, 255, 0.9);
    }
  }
  }

@keyframes pulse-green {
  0% {
    transform: translate(-50%, -50%) scale(0.8);
    opacity: 0.8;
  }
  70% {
    transform: translate(-50%, -50%) scale(1.5);
    opacity: 0;
  }
  100% {
    transform: translate(-50%, -50%) scale(0.8);
    opacity: 0;
  }
}

@keyframes pulse-red {
  0% {
    transform: translate(-50%, -50%) scale(0.8);
    opacity: 0.8;
  }
  70% {
    transform: translate(-50%, -50%) scale(1.5);
    opacity: 0;
  }
  100% {
    transform: translate(-50%, -50%) scale(0.8);
    opacity: 0;
  }
}

@media screen and (max-width: 1600px) {
  .grid-cols-4 {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

@media screen and (max-width: 1280px) {
  .grid-cols-4 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media screen and (max-width: 768px) {
  .grid-cols-4 {
    grid-template-columns: repeat(1, minmax(0, 1fr));
  }
}

// 滚动到底部按钮动画
.filtered-scroll-btn {
  position: relative;
  overflow: hidden;
  transition: all 0.4s cubic-bezier(0.25, 1, 0.5, 1);
  transform-origin: center center;
  box-shadow: 0 0 15px rgba(0, 140, 255, 0.2);
  
  &::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: linear-gradient(
      90deg,
      transparent,
      rgba(0, 180, 255, 0.15),
      transparent
    );
    transform: translateX(-100%);
    animation: shine-effect 3s infinite;
  }
  
  &:hover {
    transform: translateY(-2px);
    box-shadow: 0 5px 15px rgba(0, 140, 255, 0.3);
    
    .el-icon {
      transform: translateY(3px);
    }
  }
  
  &:active {
    transform: translateY(1px);
    box-shadow: 0 2px 10px rgba(0, 140, 255, 0.2);
  }
  
  .el-icon {
    margin-left: 8px;
    transition: transform 0.3s ease;
    animation: bounce 2s infinite;
  }
}

// 淡入淡出效果改进
.fade-enter-active {
  transition: all 0.4s cubic-bezier(0.25, 1, 0.5, 1);
  animation: glow-in 0.4s ease-out;
}

.fade-leave-active {
  transition: all 0.4s cubic-bezier(0.25, 1, 0.5, 1);
  animation: glow-out 0.4s ease-in;
}

.fade-enter-from, 
.fade-leave-to {
  opacity: 0;
  transform: translateY(20px) scale(0.95);
}

@keyframes glow-in {
  0% {
    opacity: 0;
    filter: blur(5px);
    transform: translateY(20px) scale(0.95);
  }
  50% {
    filter: blur(0);
  }
  100% {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

@keyframes glow-out {
  0% {
    opacity: 1;
    filter: blur(0);
    transform: translateY(0) scale(1);
  }
  100% {
    opacity: 0;
    filter: blur(3px);
    transform: translateY(20px) scale(0.95);
  }
}

@keyframes shine-effect {
  0% {
    transform: translateX(-100%);
  }
  20%, 100% {
    transform: translateX(100%);
  }
}

@keyframes bounce {
  0%, 20%, 50%, 80%, 100% {
    transform: translateY(0);
  }
  40% {
    transform: translateY(-3px);
  }
  60% {
    transform: translateY(-1px);
  }
}
 
</style>
 